Published on

Github Action CI/CD

Authors
  • avatar
    Name
    Prover
    Twitter

Github Action 实现 CI/CD

服务技术栈:JDK17 + Springboot3 + Maven + Docker

环境准备:

  1. 服务器上安装下 docker, nginx 然后 ssh 打开即可
  2. runner 的话可以用 github 官方的(每个月都 2000 分钟的限制),也可以用自己电脑当作的 runner(无限制)

说明:

  • 主要是通过 github action 实现镜像的构建和上传
  • 部署主要是通过远程执行服务器脚本,然后传入参数,拉取指定镜像并运行容器 + 重启 nginx 服务器
  • 这里的话主要是通过容器暴漏指定端口,然后把 nginx 对应的 url 前缀映射到容器上

实现步骤:

  1. 准备一个镜像仓库:我这里用的阿里云 acr

    1. 创建一个个人版实例

    2. 创建一个镜像仓库,可以和服务名保持一致

    3. 创建一个命名空间

    4. 在访问凭证中设置固定密码

      最后要上传的仓库地址可以打开镜像仓库看到

      image-20250406235117702

  2. [可选] 准备 runner 的环境,因为我这里是 windows 所以单独说一下

    1. 安装 windows 版的 docker 服务

    2. 安装 windows ssh 客户端服务, 打开 管理员PowerShell

      1. 查看是否安装了

        Get-WindowsCapability -Online | ? Name -like 'OpenSSH*'
        
      2. 这里只需要安装 ssh 客户端

        Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
        
    3. 要在 windows 上生成 ssh 密钥,然后把公钥拷贝到服务上,注意不要重名把服务器的公钥覆盖了

      image-20250406234355558

      然后执行两个命令把公钥加到 authorizad_keys

      cat 你上传的公钥名.pub >> authorized_keys  
      echo -e "#" >> authorized_keys
      
    4. 准备 runner 环境,这个很简单:打开 Setting -> Actions -> Runners -> New self-hosted runner 按照其中的步骤即可添加自己的 runner

  3. 将 docker 仓库的信息保存到 github 仓库的 Action secrets 中,这样就可以在工作流脚本中使用

    image-20250406234712435

  4. 在远程服务器上编写适合自己项目的部署脚本,我这里放在了 /usr/local/deplpy.sh

    # 拉取镜像并启动容器
    docker pull $IMAGE
    docker stop $PROJECT_NAME || true
    docker rm $PROJECT_NAME || true
    docker run -d --name $PROJECT_NAME -p $APP_PORT:$APP_PORT $IMAGE
    docker rmi $IMAGE || true
    
    # 动态生成 Nginx 配置
    # NGINX_TEMPLATE="/usr/local/nginx/conf.d/project.conf.template"
    NGINX_MAIN_CONF="/usr/local/nginx/conf/nginx.conf"
    TMP_CONF="/tmp/nginx_tmp.conf"
    LOCK_FILE="/tmp/nginx_conf.lock"
    
    ESCAPED_PREFIX=$(echo "$URL_PREFIX" | sed 's/\//\\\//g')
    
    (
      flock -x -w 30 200 || { echo "[ERROR] 获取文件锁超时"; exit 1; }
    
      # 转义 URL 前缀中的特殊字符(/ { } 等)
      ESCAPED_PREFIX=$(echo "$2" | sed 's/\//\\\//g; s/{/\\{/g; s/}/\\}/g')
    
      # 步骤1:生成临时文件并删除旧配置(如果存在)
      if grep -q "location $ESCAPED_PREFIX {" $NGINX_MAIN_CONF; then
        sed "/location $ESCAPED_PREFIX {/,/}/d" $NGINX_MAIN_CONF > $TMP_CONF
      else
        cp $NGINX_MAIN_CONF $TMP_CONF
      fi
    
      # 步骤2:插入新配置到 server 块内
      sed -i "/server {/a\\
          location $2 {\\
              proxy_pass http://localhost:$3;\\
              proxy_set_header Host \$host;\\
              proxy_set_header X-Real-IP \$remote_addr;\\
              proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\\
          }" $TMP_CONF
    
      # 步骤3:检查临时文件有效性
      if [ ! -s $TMP_CONF ] || ! grep -q "server {" $TMP_CONF; then
        echo "[ERROR] 临时配置文件无效,拒绝覆盖原文件!"
        exit 1
      fi
    
      # 步骤4:替换原配置
      mv $TMP_CONF $NGINX_MAIN_CONF
    
      # 4. 验证并重载 Nginx
      /usr/local/nginx/sbin/nginx -t && /usr/local/nginx/sbin/nginx -s reload
    ) 200>$LOCK_FILE
    
  5. 编写 github 工作流脚本,在项目的根目录下创建一个 .github/workflows/docker-build-deploy.yml

    name: Docker Build, Push & Deploy
    
    on:
      push:
        branches: [ "main" ]  # 触发分支(按需修改)
      workflow_dispatch:      # 允许手动触发
    
    env:
      DOCKER_REGISTRY: 你的镜像仓库地址             
      IMAGE_NAME: 镜像名称     
      TAG: ${{ github.sha  }}
      IMAGE_NAME_SPACE: 命名空间
      URL_PREFIX: 服务路径
      APP_PORT: 服务端口
    
    jobs:
      build-and-push:
        runs-on: self-hosted
        steps:
          - name: Checkout Code
            uses: actions/checkout@v4
    
          # 使用 setup-java 内置缓存功能
          - name: Set up JDK with Caching
            uses: actions/setup-java@v4
            with:
              distribution: 'temurin'
              java-version: '17'
              cache: maven
              
          - name: Set up Docker Buildx
            uses: docker/setup-buildx-action@v2
    
          - name: Login to Docker Registry
            uses: docker/login-action@v2
            with:
              registry: ${{ env.DOCKER_REGISTRY }}
              username: ${{ secrets.DOCKER_USERNAME }}
              password: ${{ secrets.DOCKER_PASSWORD }}
              
          - name: Build and Push Docker Image
            uses: docker/build-push-action@v4
            with:
              network: host
              context: .
              file: ./Dockerfile
              push: true
              tags: |
                ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME_SPACE }}/${{ env.IMAGE_NAME }}:${{ env.TAG }}
                ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME_SPACE }}/${{ env.IMAGE_NAME }}:latest
              cache-from: type=gha,timeout=120m
              cache-to: type=gha,mode=max,timeout=120m
    
          - name: SSH Deploy
            run: |
              ssh root@你的服务器地址 -i id_ed25519 "
                /usr/local/deploy.sh \
                ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME_SPACE }}/${{ env.IMAGE_NAME }}:${{ env.TAG }} \
                ${{ env.URL_PREFIX }} \
                ${{ env.APP_PORT }} \
                ${{ env.IMAGE_NAME }}
              "
    
    • 注意:如果你的 runner 环境是 Linux 服务器,那么最后的 ssh 部署的命令不太一样,可以用 appleboy/ssh-action@v1 插件实现
  6. 给项目编写一个 Dockerfile

    # 第一阶段:使用 Maven 构建 JAR(构建环境)
    FROM maven:3.9-eclipse-temurin-17-alpine AS builder
    
    # 设置工作目录
    WORKDIR /app
    
    # 复制 POM 和源码(利用 Docker 层缓存优化)
    COPY pom.xml .
    RUN mvn dependency:go-offline -DskipTests
    
    COPY src ./src
    RUN mvn clean package -DskipTests
    
    # 第二阶段:运行 JAR(生产环境)
    FROM eclipse-temurin:17-jre-alpine
    
    # 设置时区(可选)
    ENV TZ=Asia/Shanghai
    RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
    
    # 设置工作目录
    WORKDIR /app
    
    # 从构建阶段复制 JAR 文件
    COPY --from=builder /app/target/*.jar app.jar
    
    # 暴露端口(与 Spring Boot 配置的端口一致)
    EXPOSE 8080
    
    # 启动应用(优化 JVM 参数)
    ENTRYPOINT ["java", "-jar", "app.jar"]
    
  7. 然后提交代码到 main 分支上即可运行啦

遗留问题:

  1. github docker build 每隔一段时间都要重新下载其中的 maven 依赖,无语了,不知道怎么搞
  2. 部署脚本还有点问题,如果有注释的 server 配置,加的时候会报错
  3. 一些 Dockerfile 的参数可以放到环境变量中
加载中...